﻿using System;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Web;

namespace NSuggest.Jsonp.Core
{
	public delegate void ErrorDelegate(object o, ErrorEventArgs e);
	public delegate void SuccessDelegate(object o, SuccessEventArgs e);
	public delegate void CompletedDelegate(object o, CompletedEventArgs e);

	/// <summary>
	/// The core class in JsonpNet. The main functionality is to consume HttpContext objects, and writing serialized business objects to the Response property of the HttpContext.
	/// </summary>
	public class JsonpEngine
	{

		// Events
		public event ErrorDelegate ErrorEvent;
		public event SuccessDelegate SuccessEvent;
		public event CompletedDelegate CompletedEvent;

		public Func<HttpContext, object> Query { get; set; }
		public Func<HttpContext, bool> Authorized { get; set; }
		public string CallbackParameter { get; set; }

		/// <summary>
		/// Initializes a new instance of the <see cref="JsonpEngine"/> class.
		/// </summary>
		/// <param name="getResult">The only required parameter. A function type that takes a HttpContext object and returns objects.</param>
		/// <param name="authorized">Optional parameter. Provide a means to implement custom authorization logic.</param>
		/// <param name="callbackParameter">Optional name of the callback parameter, used when calling the JSONP service. The default is "callback".</param>
		public JsonpEngine(Func<HttpContext, object> getResult, Func<HttpContext, bool> authorized = null, string callbackParameter = "callback")
		{
			Query = getResult ?? (context => default(object)) ;
			Authorized = authorized ?? (context => true) ;
			CallbackParameter = callbackParameter;
		}

		/// <summary>
		/// Consume a HttpContext object, perform business logic and write JSONP to the Response.
		/// </summary>
		/// <param name="context">a HttpContext object passed by e.g. a generic handler.</param>
		public void Execute(HttpContext context)
		{

			// Get the callback parameter from the querystring.
			var callback = context.Request.QueryString[CallbackParameter];

			try
			{
				// Unauthorized?
				if (Authorized != null && !Authorized(context))
				{
					throw new HttpException(401, "Not authorized to use this service");
				}

				// Callback provided?
				if (callback == null)
				{
					throw new HttpException(400, string.Format("Required parameter '{0}' missing. This is the name of the callback to invoke.", CallbackParameter));
				}

				// Get result from business logic
				var result = Query(context); // may throw HttpException or Exception
				// Serialize the result to JSON
				var json = SerializeToJson(result); // may throw Exception

				// Invoke success event... no exception thrown while handling request.
				InvokeSuccessEvent(new SuccessEventArgs(result));

				// Set response headers
				context.Response.StatusCode = 200;
				context.Response.StatusDescription = "200 OK";
				context.Response.ContentType = "text/javascript";

				// Write data to response
				context.Response.Write(string.Format("{0}({1})", callback, json));
			}

			catch (Exception exception)
			{

				// Handle error. Return the HTTP status code.
				var statusCode = HandleError(context, exception);
				// Invoke error event
				InvokeErrorEvent(new ErrorEventArgs(exception, statusCode));
			}

			// Invoke complete event
			InvokeCompletedEvent(new CompletedEventArgs());
		}

		/// <summary>
		/// If an exception occours when generating the Response, this method is called. It will write an HTTP error to the Response.
		/// </summary>
		/// <param name="context">The context.</param>
		/// <param name="exception">The exception.</param>
		private static int HandleError(HttpContext context, Exception exception)
		{
			var httpException = exception as HttpException;
			if (httpException != null)
			{
				context.Response.StatusCode = httpException.GetHttpCode();
				context.Response.StatusDescription = httpException.Message;
				context.Response.Write(httpException.GetHtmlErrorMessage() ?? string.Format(@"<html><head></head><body>{0}</body></html>", exception.Message));
			}
			else
			{
				context.Response.StatusCode = 500;
				context.Response.StatusDescription = "500 Internal Server Error";
				context.Response.Write(string.Format(@"<html><head></head><body>Unknown error: {0}</body></html>", exception.Message));
			}
			return context.Response.StatusCode;
		}

		/// <summary>
		/// Utility method for serializing object to JSON. In order to work, object must use the DataContract/DataMember attributes.
		/// </summary>
		/// <param name="obj">The business object to serialize.</param>
		/// <returns></returns>
		private static string SerializeToJson(object obj)
		{
			var serializer = new DataContractJsonSerializer(obj.GetType());
			var ms = new MemoryStream();
			serializer.WriteObject(ms, obj);
			var json = Encoding.Default.GetString(ms.ToArray());
			return json;
		}

		/// <summary>
		/// Invokes the success event.
		/// </summary>
		private void InvokeSuccessEvent(SuccessEventArgs e)
		{
			var handler = SuccessEvent;
			if (handler != null)
			{
				handler(null, new SuccessEventArgs(e.Result));
			}
		}

		/// <summary>
		/// Invokes the completed event.
		/// </summary>
		private void InvokeCompletedEvent(CompletedEventArgs e)
		{
			var handler = CompletedEvent;
			if (handler != null)
			{
				handler(null, e);
			}
		}

		/// <summary>
		/// Invokes the error event.
		/// </summary>
		private void InvokeErrorEvent(ErrorEventArgs e)
		{
			var handler = ErrorEvent;
			if (handler != null) handler(null, e);
		}

	}
}
